/*
 * Copyright (C) 2009,2010,2011  Paulo Cesar Pereira de Andrade.
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Authors:
 *	Paulo Cesar Pereira de Andrade
 */

#include "exl.h"

/*
 * Prototypes
 */
static void
fold(efunction_t *function);

static ebool_t
fold_node_to_value(east_node_t *node, evalue_t *value);

static east_node_t *
fold_value_to_node(evalue_t *value);

static void
fold_value_copy_and_push(evalue_t *value);

static ebool_t
fold_bool(ecode_t code, east_list_t *base, ebool_t constant);

static east_node_t *
fold_unary(east_node_t *node, east_node_t *arg0);

static east_node_t *
fold_unary_wrapped(ecode_t code, east_node_t *arg0);

static east_node_t *
fold_inc_dec(ecode_t code, east_node_t *arg0);

static east_node_t *
fold_binary(east_node_t * node, east_node_t *arg0, east_node_t *arg1);

static east_node_t *
fold_binary_wrapped(ecode_t code, east_node_t *arg0, east_node_t *arg1);

/*
 * Initialization
 */
static ebool_t		again;
static ebool_t		first;

/*
 * Implementation
 */
ebool_t
efold(efunction_t *function)
{
    ebool_t		change;

    /* should never need, but check once for safety */
    v_check(4);

    again = false;
    first = cfg_pass == 0;
    fold(function);
    first = false;
    if ((change = again)) {
	do {
	    again = false;
	    fold(function);
	} while (again);
    }

    return (change);
}

static void
fold(efunction_t *function)
{
    east_node_t		*arg0;
    east_node_t		*arg1;
    east_list_t		*base;
    east_list_t		*list;
    east_node_t		*node;
    east_list_t		*push;
    east_list_t		*prev;
    eint32_t		 state;
    eint32_t		 store;

#define have_none	0
#define have_arg0	1
#define wait_arg1	2
#define have_arg1	4

#define stor_none	0
#define stor_arg0	1
#define stor_arg1	2
    cfg_note = null;
    state = have_none;
    store = stor_none;
    arg0 = arg1 = null;
    base = push = null;
    prev = function->code;
    for (list = east_next(prev); list; prev = list, list = east_next(list)) {
	node = east_node(list);
	switch (node->code) {
	    case eop_nil:			case eop_int:
	    case eop_float:			case eop_ll:
		if (state == wait_arg1) {
		    arg1 = node;
		    state = have_arg1;
		}
		else {
		    base = list;
		    arg0 = node;
		    state = have_arg0;
		    store = stor_none;
		}
		break;
	    case eop_push:
		if (state == have_arg0 &&
		    node->cstack.flags & code_stack_binarg) {
		    state = wait_arg1;
		    if (store == stor_arg0)
			push = prev;
		}
		else
		    state = have_none;
		break;
	    case eop_sd:			case eop_sdt:
	    case eop_sb:			case eop_sbt:
	    case eop_ss:			case eop_sst:
	    case eop_ssv:			case eop_svai:
		if (state == have_arg0)
		    store = stor_arg0;
		else if (state == have_arg1)
		    store |= stor_arg1;
		break;
	    case eop_bool:			case eop_not:
		if ((fold_bool(node->code, prev, state == have_arg0))) {
		    list = prev;
		    state = have_none;
		    continue;
		}
	    case eop_com:			case eop_inv:
	    case eop_neg:			case eop_signbit:
	    case eop_signum:			case eop_rational:
	    case eop_integer_p:			case eop_rational_p:
	    case eop_float_p:			case eop_real_p:
	    case eop_complex_p:			case eop_number_p:
	    case eop_finite_p:			case eop_inf_p:
	    case eop_nan_p:			case eop_num:
	    case eop_den:			case eop_real:
	    case eop_imag:			case eop_arg:
	    case eop_conj:			case eop_floor:
	    case eop_trunc:			case eop_round:
	    case eop_ceil:			case eop_abs:
	    case eop_sqrt:			case eop_cbrt:
	    case eop_sin:			case eop_cos:
	    case eop_tan:			case eop_asin:
	    case eop_acos:			case eop_atan:
	    case eop_sinh:			case eop_cosh:
	    case eop_tanh:			case eop_asinh:
	    case eop_acosh:			case eop_atanh:
	    case eop_proj:			case eop_exp:
	    case eop_log:			case eop_log2:
	    case eop_log10:
		if (state == have_arg0 &&
		    (node = fold_unary(node, arg0))) {
		    if (store) {
			east_next(prev) = east_next(list);
			east_node(prev) = arg0 = node;
			store = stor_none;
			list = base = prev;
		    }
		    else {
			east_next(base) = east_next(list);
			east_node(base) = arg0 = node;
			list = base;
		    }
		    again = true;
		}
		else
		    state = have_none;
		break;
	    case eop_inc:			case eop_dec:
		if (state == have_arg0 &&
		    (node = fold_inc_dec(node->code, arg0))) {
		    if (store) {
			east_next(prev) = east_next(list);
			east_node(prev) = arg0 = node;
			store = stor_none;
			list = base = prev;
		    }
		    else {
			east_next(base) = east_next(list);
			east_node(base) = arg0 = node;
			list = base;
		    }
		    again = true;
		}
		else
		    state = have_none;
		break;
	    case eop_ne:			case eop_lt:
	    case eop_le:			case eop_eq:
	    case eop_ge:			case eop_gt:
	    case eop_and:			case eop_or:
	    case eop_xor:			case eop_mul2:
	    case eop_div2:			case eop_shl:
	    case eop_shr:			case eop_add:
	    case eop_sub:			case eop_mul:
	    case eop_div:			case eop_trunc2:
	    case eop_rem:			case eop_atan2:
	    case eop_pow:			case eop_hypot:
	    case eop_complex:
		if (state == have_arg1 &&
		    (node = fold_binary(node, arg0, arg1))) {
		    if (store == stor_arg0) {
			push = east_next(push);
			east_next(push) = east_next(list);
			east_node(push) = arg0 = node;
			store = stor_none;
			list = base = push;
		    }
		    else if (store) {
			east_next(push) = east_next(east_next(push));
			east_node(list) = arg0 = node;
			store = stor_none;
			base = list;
		    }
		    else {
			east_next(base) = east_next(list);
			east_node(base) = arg0 = node;
			list = base;
		    }
		    state = have_arg0;
		    again = true;
		}
		else
		    state = have_none;
		break;
	    case eop_j:
		/* basic block may end in an orphan constant load
		 * after basic jump optimization */
		if (state == have_arg0 && store == stor_none &&
		    !ecfg_implicit_used_p(east_next(elabel_list(node->clabel.label)))) {
		    east_next(base) = east_next(list);
		    east_node(base) = node;
		    list = base;
		    again = true;
		}
		state = have_none;
		break;
	    case eop_note:
		cfg_note = node;
		break;
	    default:
		state = have_none;
		break;
	}
    }
}

static ebool_t
fold_node_to_value(east_node_t *node, evalue_t *value)
{
    eobject_t		object;

    switch (node->code) {
	case eop_nil:
	    value->t = t_int;
	    value->v.i = 0;
	    break;
	case eop_int:
	    value->t = t_int;
	    value->v.i = immediate_int(node);
	    break;
	case eop_float:
	    value->t = t_float;
	    value->v.d = immediate_float(node);
	    break;
	default:
	    assert(node->code == eop_ll);
	    object = els[immediate_offset(node)];
	    switch (value->t = etype(object)) {
		case t_mpz:			case t_mpq:
		case t_mpr:			case t_cqq:
		case t_mpc:
		    value->v.o = object;
		    break;
		case t_cdd:
		    value->t = t_cdd;
		    value->v.cdd = *(ecdd_t *)object;
		    break;
		default:
		    value->v.o = object;
		    return (false);
	    }
	    break;
    }

    return (true);
}

static east_node_t *
fold_value_to_node(evalue_t *value)
{
    east_node_t		*node;

    switch (value->t) {
	case t_int:
	    node = ecode_int(value->v.i);
	    break;
	case t_float:
	    node = ecode_float(value->v.d);
	    break;
	case t_cdd:
	    enew_cdd(&value->v.o, value->v.cdd);
	case t_mpz:		case t_mpq:
	case t_mpr:		case t_cqq:
	case t_mpc:
	    v_push(value->v.o);
	    node = ecode_literal(econstant(value->v.o));
	    break;
	default:
	    abort();
    }

    return (node);
}

static void
fold_value_copy_and_push(evalue_t *value)
{
    switch (value->t) {
	case t_mpz:	enew_mpz(&value->v.o, value->v.mpz);	break;
	case t_mpq:	enew_mpq(&value->v.o, value->v.mpq);	break;
	case t_mpr:	enew_mpr(&value->v.o, value->v.mpr);	break;
	case t_cqq:	enew_cqq(&value->v.o, value->v.cqq);	break;
	case t_mpc:	enew_mpc(&value->v.o, value->v.mpc);	break;
	default:
	    return;
    }
    v_push(value->v.o);
}

static ebool_t
fold_bool(ecode_t code, east_list_t *base, ebool_t constant)
{
    east_node_t		*fold;
    east_list_t		*list;
    east_node_t		*node;
    east_list_t		*prev;
    elabel_t		*label;

    prev = east_next(base);
    fold = east_node(prev);
    for (list = east_next(prev); list; prev = list, list = east_next(list)) {
	node = east_node(list);
	switch (node->code) {
	    case eop_not:
		code = code == eop_not ? eop_bool : eop_not;
		fold->code = code;
	    case eop_bool:
		east_next(prev) = east_next(list);
		list = prev;
		again = true;
		break;
	    case eop_jt:			case eop_jf:
		label = node->clabel.label;
		if (/* will fold constant expression */
		    constant ||
		    /* code after jump uses computed value */
		    ecfg_implicit_used_p(east_next(list)) ||
		    /* code after jump target uses computed value */
		    ecfg_implicit_used_p(east_next(elabel_list(label))))
		    return (false);
		/* exp, not, jt => exp, jf */
		/* exp, bool, jt => exp, jt */
		if (code == eop_not)
		    node->code = node->code == eop_jt ? eop_jf : eop_jt;
		east_next(base) = east_next(east_next(base));
		again = true;
		return (true);
	    case eop_note:
		break;
	    default:
		return (false);
	}
    }

    return (false);
}

static east_node_t *
fold_unary(east_node_t *node, east_node_t *arg0)
{
    v_enter();
    if (node->clogic.flags & code_logic_except)
	return (null);
    if (sigsetjmp(thread_self->env, 1)) {
	if (first)
	    ecfg_warn(parse, "constant folding exception");
	node->clogic.flags |= code_logic_except;
	node = null;
    }
    else
	node = fold_unary_wrapped(node->code, arg0);
    /* restore previous exception */
    memcpy(&thread_self->env, &cfg_env, sizeof(sigjmp_buf));
    v_leave();
    return (node);
}

static east_node_t *
fold_unary_wrapped(ecode_t code, east_node_t *arg0)
{
    east_node_t		*node;
    evalue_t		 value;

    /* ensure argument is numeric */
    if (!fold_node_to_value(arg0, &value)) {

	/* check for valid operations on non numeric arguments */
	switch (code) {
	    case eop_not:
		value.v.i = 0;
		break;
	    case eop_bool:
		value.v.i = 1;
		break;
	    default:
		/* FIXME folding of vector operations not implemented */
		if (evector_type_p(value.t))
		    return (null);
		ecfg_error(parse, "not a number");
	}
	value.t = t_int;

	return (fold_value_to_node(&value));
    }

    /* check if need to duplicate object before inplace operation */
    switch (code) {
	case eop_bool:		case eop_not:
	case eop_signbit:	case eop_integer_p:
	case eop_rational_p:	case eop_float_p:
	case eop_real_p:	case eop_complex_p:
	case eop_number_p:	case eop_finite_p:
	case eop_inf_p:		case eop_nan_p:
	    break;
	default:
	    fold_value_copy_and_push(&value);
	    break;
    }

    switch (code) {
	case eop_bool:		enumber_bool(&value);		break;
	case eop_not:		enumber_not(&value);		break;
	case eop_com:		einteger_com(&value);		break;
	case eop_neg:		enumber_neg(&value);		break;
	case eop_signbit:	enumber_signbit(&value);	break;
	case eop_signum:	enumber_signum(&value);		break;
	case eop_rational:	enumber_rational(&value);	break;
	case eop_integer_p:	enumber_integer_p(&value);	break;
	case eop_rational_p:	enumber_rational_p(&value);	break;
	case eop_float_p:	enumber_float_p(&value);	break;
	case eop_real_p:	enumber_real_p(&value);		break;
	case eop_complex_p:	enumber_complex_p(&value);	break;
	case eop_number_p:	enumber_number_p(&value);	break;
	case eop_finite_p:	enumber_finite_p(&value);	break;
	case eop_inf_p:		enumber_inf_p(&value);		break;
	case eop_nan_p:		enumber_nan_p(&value);		break;
	case eop_inv:		enumber_inv(&value);		break;
	case eop_num:		enumber_num(&value);		break;
	case eop_den:		enumber_den(&value);		break;
	case eop_real:		enumber_real(&value);		break;
	case eop_imag:		enumber_imag(&value);		break;
	case eop_arg:		enumber_arg(&value);		break;
	case eop_conj:		enumber_conj(&value);		break;
	case eop_floor:		enumber_floor(&value);		break;
	case eop_trunc:		enumber_trunc(&value);		break;
	case eop_round:		enumber_round(&value);		break;
	case eop_ceil:		enumber_ceil(&value);		break;
	case eop_abs:		enumber_abs(&value);		break;
	case eop_sqrt:		enumber_sqrt(&value);		break;
	case eop_cbrt:		enumber_cbrt(&value);		break;
	case eop_sin:		enumber_sin(&value);		break;
	case eop_cos:		enumber_cos(&value);		break;
	case eop_tan:		enumber_tan(&value);		break;
	case eop_asin:		enumber_asin(&value);		break;
	case eop_acos:		enumber_acos(&value);		break;
	case eop_atan:		enumber_atan(&value);		break;
	case eop_sinh:		enumber_sinh(&value);		break;
	case eop_cosh:		enumber_cosh(&value);		break;
	case eop_tanh:		enumber_tanh(&value);		break;
	case eop_asinh:		enumber_asinh(&value);		break;
	case eop_acosh:		enumber_acosh(&value);		break;
	case eop_atanh:		enumber_atanh(&value);		break;
	case eop_proj:		enumber_proj(&value);		break;
	case eop_exp:		enumber_exp(&value);		break;
	case eop_log:		enumber_log(&value);		break;
	case eop_log2:		enumber_log2(&value);		break;
	default:
	    assert(code == eop_log10);
	    enumber_log10(&value);
	    break;
    }

    node = fold_value_to_node(&value);

    return (node);
}

static east_node_t *
fold_inc_dec(ecode_t code, east_node_t *arg0)
{
    east_node_t		*node;
    evalue_t		 val0;
    evalue_t		 val1;
    v_enter();

    /* ensure arguments are numeric */
    if (!fold_node_to_value(arg0, &val0))
	ecfg_error(parse, "not a number");

    fold_value_copy_and_push(&val0);

    val1.t = t_int;
    val1.v.i  = 1;

    if (val0.t != t_int) {
	enumber_coerce(&val0, &val1);
	v_leave();
	switch (val0.t) {
	    case t_mpz: case t_mpq: case t_mpr: case t_cqq: case t_mpc:
		v_push(val0.v.o);
		v_push(val1.v.o);
		break;
	    default:
		break;
	}
    }

    if (code == eop_inc)
	enumber_add(&val0, &val1);
    else {
	assert(code == eop_dec);
	enumber_sub(&val0, &val1);
    }

    node = fold_value_to_node(&val0);
    v_leave();

    return (node);
}

static east_node_t *
fold_binary(east_node_t *node, east_node_t *arg0, east_node_t *arg1)
{
    v_enter();
    if (node->clogic.flags & code_logic_except)
	return (null);
    if (sigsetjmp(thread_self->env, 1)) {
	if (first)
	    ecfg_warn(parse, "constant folding exception");
	node->clogic.flags |= code_logic_except;
	node = null;
    }
    else
	node = fold_binary_wrapped(node->code, arg0, arg1);
    /* restore previous exception */
    memcpy(&thread_self->env, &cfg_env, sizeof(sigjmp_buf));
    v_leave();
    return (node);
}

static east_node_t *
fold_binary_wrapped(ecode_t code, east_node_t *arg0, east_node_t *arg1)
{
    ebool_t		 con0;
    ebool_t		 con1;
    east_node_t		*node;
    evalue_t		 val0;
    evalue_t		 val1;
    evector_t		*vec0;
    evector_t		*vec1;

    /* ensure arguments are numeric */
    con0 = fold_node_to_value(arg0, &val0);
    con1 = fold_node_to_value(arg1, &val1);
    if (!con0 || !con1) {
	/* check for valid operations on non numeric arguments */
	switch (code) {
	    case eop_eq:	case eop_ne:
		if ((val0.t & ~t_const) == (val1.t & ~t_const)) {
		    if ((val0.t & ~t_const) == t_string) {
			vec0 = val0.v.o;
			vec1 = val1.v.o;
			if (vec0->length == vec1->length &&
			    memcmp(vec0->v.obj, vec1->v.obj, vec0->length) == 0)
			    val0.v.i = code == eop_eq;
			else
			    val0.v.i = code != eop_eq;
		    }
		    else if (val0.v.o == val1.v.o)
			val0.v.i = code == eop_eq;
		    else
			val0.v.i = code != eop_eq;
		}
		else
		    val0.v.i = code != eop_eq;
		break;
	    default:
		if ((!con0 && !evector_type_p(val0.t)) ||
		    (!con1 && !evector_type_p(val1.t)))
		    ecfg_error(parse, "not a number");
		/* FIXME folding of vector operations not implemented */
		return (null);
	}
	val0.t = t_int;

	return (fold_value_to_node(&val0));
    }

    switch (code) {
	case eop_ne:		case eop_lt:
	case eop_le:		case eop_eq:
	case eop_ge:		case eop_gt:
	    break;
	case eop_complex:
	    if (ecode_false_p(arg1))
		return (arg0);
	default:
	    fold_value_copy_and_push(&val0);
	    break;
    }

    if (val0.t != val1.t) {
	switch (code) {
	    case eop_mul2:	case eop_div2:
	    case eop_shl:	case eop_shr:
		break;
	    default:
		enumber_coerce(&val0, &val1);
		switch (val0.t) {
		    case t_mpz: case t_mpq: case t_mpr: case t_cqq: case t_mpc:
			v_push(val0.v.o);
			v_push(val1.v.o);
			break;
		    default:
			break;
		}
		break;
	}
    }

    switch (code) {
	case eop_ne:		enumber_ne(&val0, &val1);	break;
	case eop_lt:		enumber_lt(&val0, &val1);	break;
	case eop_le:		enumber_le(&val0, &val1);	break;
	case eop_eq:		enumber_eq(&val0, &val1);	break;
	case eop_ge:		enumber_ge(&val0, &val1);	break;
	case eop_gt:		enumber_gt(&val0, &val1);	break;
	case eop_and:		einteger_and(&val0, &val1);	break;
	case eop_or:		einteger_or(&val0, &val1);	break;
	case eop_xor:		einteger_xor(&val0, &val1);	break;
	case eop_mul2:		enumber_mul2(&val0, &val1);	break;
	case eop_div2:		enumber_div2(&val0, &val1);	break;
	case eop_shl:		enumber_shl(&val0, &val1);	break;
	case eop_shr:		enumber_shr(&val0, &val1);	break;
	case eop_add:		enumber_add(&val0, &val1);	break;
	case eop_sub:		enumber_sub(&val0, &val1);	break;
	case eop_mul:		enumber_mul(&val0, &val1);	break;
	case eop_div:		enumber_div(&val0, &val1);	break;
	case eop_trunc2:	enumber_trunc2(&val0, &val1);	break;
	case eop_rem:		enumber_rem(&val0, &val1);	break;
	case eop_atan2:		enumber_atan2(&val0, &val1);	break;
	case eop_pow:		enumber_pow(&val0, &val1);	break;
	case eop_hypot:		enumber_hypot(&val0, &val1);	break;
	default:
	    assert(code == eop_complex);
	    enumber_complex(&val0, &val1);
	    break;
    }

    node = fold_value_to_node(&val0);

    return (node);
}
